引用
一个引用就是某对象的另一个名字。引用的主要用途是为了描述函数的参数和返回值,特别是为了运算符的重载(第11章)。记法X&表示到X的引用。例如,
void f()
{
int i = 1;
int& r = i; // r和i现在引用同一个int
int x = r; // x = 1
r = 2; // i = 2
}
为了确保一个引用总能是某个东西的名字(也就是说,总能约束到某个对象),我们必须对引用做初始化。例如,
int i = 1;
int& r1 = i; // 正确:r1被初始化
int& r2; // 错误❌:没有初始化
extern int& r3; // 正确:r3在别处初始化
对一个引用的初始化是与对它赋值完全不同的另一件事情。除了外表形式之外,实际上根本就没有能操作引用的运算符操作。例如,
void g()
{
int ii = 0;
int& rr = ii;
rr++; // ii被增加1
int* pp = &rr; // pp指向ii
}
这些都合法,但是rr++并没有对引用本身做什么增量操作;相反,++是应用到了那个int上,而这个int碰巧就是ii。因此,一个引用的值在初始化之后就不可能改变了,它总是引用它的初始化所指称的那个对象。要取得被引用rr所引用对象的地址,我们可以写&rr。
引用的一种最明显的实现方式是作为一个(常量)指针,在每次使用它时都自动地做间接访问。将引用想象成这种样子不会有任何问题,但要记住的是,一个引用并不是一个对象,不能像指针那样去操作。
在一些情况下,编译器可以通过优化去掉引用,使得在执行时根本不存在任何表示引用的东西。
当引用的初始式是一个左值时(是一个对象,你可以取得它的地址,见4.9.6节),其初始化就是非常简单的事情。对“普通”T&的初始式必须是一个类型T的左值。
对一个const T&的初始式不必是一个左值,甚至可以不是类型T的;在这种情况下:
1)、首先,如果需要将应用到T的隐式类型转换(见C.6节)。
2)、而后将结果存入一个类型T的临时变量。
3)、最后,将此临时变量用做初始式的值。
考虑
double& dr = 1; // 错误❌:要求左值
const double& cdr = 1; // ok
对后一个初始化的解释是
double temp = double(1); // 首先建立一个具有正确值的临时变量
const double& cdr = temp; // 而后用这个临时变量作为cdr的初始式
这种保存引用初始式的临时变量将一直存在,直到这个引用的作用域结束。
需要区分对变量的引用和对常量的引用,是因为在变量引用的情况下引进临时量极易出错,对变量的赋值将会变成对于---即将消失的---临时量的赋值。对于常量引用则不会有这类问题,以常量的引用作为函数参数经常是很重要的(11.6节)。
可以通过引用来描述一个函数参数,以使该函数能够改变传递来的变量的值。例如,
void increment(int& aa) { aa++; }
void f()
{
int x = 1;
increment(x); // x = 2
}
参数传递的语义通过对应的初始化定义,所以,在调用时,increament的参数aa将变成x的另一个名字。为了提高程序的可读性,通常应该尽可能避免让函数去修改它们的参数。相反,你应该让函数明确地返回一个值,或者明确要求一个指针参数:
int next(int p) { return p + 1; }
void incr(int* p) { (*p)++; }
void g()
{
int x = 1;
increment(x); // x = 2
x = next(x); // x = 3
incr(&x); // x = 4
}
increament(x)的记法形式不能给读程序的人有关x的值可能被修改的提示性信息。而采用x=next(x)和incr(&x)的形式则可以。因此,如果将“普通”引用参数用于某些函数,那么这些函数的名字就应该给出其引用参数将被修改的强烈提示。
引用还可以用于定义一些函数,使它们既可以被用在赋值的左边,也可以用在右边。同样,许多最有意思的这类应用可以在比较复杂的用户定义类型中找到。作为一个例子,让我们定义一个简单的关联数组。首先,我们定义结构Pair如下:
struct Pair
{
string name;
double val;
};
基础想法就是让每个string有一个关联于它的浮点值。很容易定义一个函数value(),让它维护一个Pair的结构数据,由曾经提供给它的所有不同的字符串组成。为缩短这个演示,我们在这里采用一个非常简单的(且低效的)实现:
vector<Pair> pairs;
double& value(const string& s)
/*
维护Pair的一个集合;
检索s,如果找到就返回其值;否则做一个新Pair并返回默认值0
*/
{
for(int i = 0; i < pairs.size(); i++)
if(s == pairs[i].name) return pairs[i].val;
Pair p = { s, 0 };
pairs.push_back(p); // 将Pair加到最后(3.7.3节)
return pairs[pairs.size() - 1].val;
}
这个功能可以被理解为一个浮点值数组,它以字符串作为下标。对于给定的参数串,value()找到对应的浮点对象(而不是对应浮点对象的值),返回到这个对象的一个引用。例如,
int main() // 统计每个单词在输入中出现的次数
{
string buf;
while(cin>>buf) value(buf)++;
for(vector<Pair>::const_iterator p = pairs.begin(); p!= pairs.end(); ++p)
cout << p->name << ": " << p->val << '\n';
}
每一次while循环从标准输入流cin将一个单词读进buf(3.6节),并更新与它关联的计数器。最后,结果的表里是输入中遇到的所有互不相同的词,最后将它们及其出现的次数打印出来。举例来说,给定输入
aa bb bb aa aa bb aa aa
程序将产生出
aa: 5
bb: 3
通过使用模板类,很容易将这个程序进一步精化为一个真正的关联数组类型,带有重载的下标运算符[](11.8节)。利用标准库的map(17.4.1节)做这件事就更容易了。
🔚